package middleware

import (
	"errors"
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
	"golang.org/x/time/rate"
	"monoblog/common"
	"monoblog/config"
	"monoblog/util"
	"net/http"
	"os"
	"strconv"
	"sync"
	"time"
)

// 为每个接口路径创建独立的限流器（键：路径，值：限流器）
var pathLimiterMap sync.Map

// 接口限流规则定义
var hotspotPaths = map[string]int{
	"monoblog/view.SendSmsCode": 120,
}

// 初始化限流器（100次/秒，r=100/s, b=20突发容量）
// return limiter, true 表示有限流，false 表示不限流
func getLimiter(handlerName string) (*rate.Limiter, bool) {
	// 获取接口路径对应的限流器
	limit, ok := hotspotPaths[handlerName]
	if !ok {
		//如果没有配置，则使用默认值
		limit, _ = strconv.Atoi(os.Getenv("DEFAULT_LIMIT"))
	}

	if limit <= 0 {
		return nil, false //不限流
	}
	value, _ := pathLimiterMap.LoadOrStore(handlerName, rate.NewLimiter(rate.Limit(limit), 20))
	return value.(*rate.Limiter), true
}

// AccessLimit 限流
// 如果没有配置'DEFAULT_LIMIT' 则不限流
// 如果有接口限流规则定义，则根据规则限流（不管有没有配置默认值'DEFAULT_LIMIT'）；
// 如果没有接口限流规则定义，则使用默认值'DEFAULT_LIMIT',此时如果没有配置'DEFAULT_LIMIT' 则不限流
// 如果想让某个接口不限流，则在hotspotPaths中配置该接口的限流值为0即可
func AccessLimit(ctx *gin.Context) {
	// 获取处理函数名
	var handlerName = ctx.HandlerName()
	var limiter, enable = getLimiter(handlerName) // 获取接口路径对应的限流器

	// 如果开启了限流
	if enable {
		// 检查是否允许请求（Allow()返回true表示未超限）
		if !limiter.Allow() {
			logrus.Infof("接口 %s 限流，当前请求被拒绝", handlerName)
			ctx.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
				"code":  429,
				"error": "接口请求过于频繁，请稍后再试",
			})
			return
		}
	}

	ctx.Next() //放行，执行下去
}

// 业务资源限流规则定义
var restrictPaths = map[string]common.RestrictPath{
	"monoblog/view.SendSmsCode": common.RestrictPath{
		ID: "#phone",
		Rules: []common.RestrictRule{
			{
				Key:     "limit-30s:%s",
				Limit:   1,
				Expired: 30 * time.Second,
				Prompt:  "30秒内只能发送1条验证码",
			},
			{
				Key:         "limit-daily:%s",
				Limit:       20,
				ExpiredExpr: common.Expr_daily,
				Prompt:      "每日最多只能发送20条验证码",
			},
		},
	},
}

// BusinessLimit 业务限流
// 如果没有配置业务资源限流规则，则不限流；
// 如果有业务资源限流规则定义，则根据规则限流；
// 如果想让某个接口不限流，则在restrictPaths中配置该接口的限流值为0即可
func BusinessLimit(ctx *gin.Context) {
	var handlerName = ctx.HandlerName()

	restrictPath, ok := restrictPaths[handlerName]

	//配置了限流规则
	if ok {
		ctx.Request.ParseForm()     //先要解析form
		var form = ctx.Request.Form // 相当于 form + query合并为Form
		var id string

		//从form中获取ID参数值，后续支持取loginUser中的值
		//解析参数，获取id
		if named, success := util.GetParamName(restrictPath.ID); success {
			id = form.Get(named)

			logrus.Debugf("BusinessLimit解析ID参数成功，ID的值解析为: %s", id)
		} else {
			logrus.Errorf("BusinessLimit解析ID参数失败，ID格式错误: %s", restrictPath.ID)
			ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
				"code":  500,
				"error": "服务器解析参数失败",
			})
			return
		}

		// ---------------------- 新增：限流逻辑开始 ----------------------
		redisClient := config.GetRedisClient() // 获取Redis客户端（项目已有配置）

		for _, rule := range restrictPath.Rules { //不是指针，所以不会修改原规则
			if rule.Prompt == "" { //提示语
				rule.Prompt = "次数超限"
			}

			if rule.Limit == 1 {
				// 1. 30秒内只能发送1条验证码
				exists, err := redisClient.SetNX(ctx, rule.GetResolvedKey(id), rule.Limit, rule.Expired).Result()
				if err != nil {
					logrus.Warnf("redis SetNX 失败, rule[ %v ]. err : %v", rule.Key, err.Error())
					break //退出循环
				}
				if !exists {
					logrus.Infof("接口 %s 次数超限，当前请求被拒绝。rule.ResolvedKey = %v", handlerName, rule.GetResolvedKey(id))
					common.Fail(ctx, rule.Prompt, errors.New("次数超限，当前请求被拒绝. rule.key="+rule.Key))
					ctx.Abort()
					return
				}
			} else if rule.Limit > 1 {
				// 2. 3小时内只能发送20条验证码
				dailyCount, err := redisClient.Incr(ctx, rule.GetResolvedKey(id)).Result()
				if err != nil {
					logrus.Warnf("redis Incr 失败: %v", err.Error())
					break
				}
				// 首次设置过期时间（避免Key永久留存）
				if int(dailyCount) == 1 {
					var deadline time.Time

					// 根据表达式设置过期时间
					switch rule.ExpiredExpr {
					case common.Expr_daily:
						deadline = util.TodayMaxTime()
					case common.Expr_weekly:
						deadline = util.WeekMaxTime(false)
					case common.Expr_monthly:
						deadline = util.MonthMaxTime()
					default:
						deadline = time.Now().Add(rule.Expired)
					}

					redisClient.ExpireAt(ctx, rule.Key, deadline)
				}

				if int(dailyCount) > rule.Limit {
					logrus.Infof("接口 %s 次数超限，当前请求被拒绝。rule.ResolvedKey = %v", handlerName, rule.GetResolvedKey(id))
					common.Fail(ctx, rule.Prompt, errors.New("次数超限，当前请求被拒绝. rule.key="+rule.Key))
					ctx.Abort()
					return
				}
			}

		}
	}

	ctx.Next() //放行，执行下去
}
